Skip to main content

Applicative functors

In the section regarding functors we've seen that we can compose an effectful program f: (a: A) => F<B> with a pure one g: (b: B) => C through the transformation of g to a function map(g): (fb: F<B>) => F<C> (if and only if F admits a functor instance).

Program fProgram gComposition
purepureg ∘ f
effectfulpure (unary)map(g) ∘ f

But g has to be unary, it can only accept a single argument as input. What happens if g accepts two arguments? Can we still transform g using only the functor instance?

Currying

First of all we need to model a function that accepts two arguments of type B and C (we can use a tuple for this) and returns a value of type D:

g: (b: B, c: C) => D

We can rewrite g using a technique called currying.

Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument. For example, a function that takes two arguments, one from B and one from C, and produces outputs in D, by currying is translated into a function that takes a single argument from C and produces as outputs functions from B to C.

(source: currying on wikipedia.org)

Thus, through currying, we can rewrite g as:

g: (b: B) => (c: C) => D

Example

interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}

const addFollower = (follower: User, user: User): User => ({
...user,
followers: [...user.followers, follower]
})

Let's refactor addFollower through currying

interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}

const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})

// -------------------
// usage example
// -------------------

const user: User = { id: 1, name: 'Ruth R. Gonzalez', followers: [] }
const follower: User = { id: 3, name: 'Marsha J. Joslyn', followers: [] }

console.log(addFollower(follower)(user))
/*
{
id: 1,
name: 'Ruth R. Gonzalez',
followers: [ { id: 3, name: 'Marsha J. Joslyn', followers: [] } ]
}
*/

The ap operation

Suppose that:

  • we do not have a follower but only his id
  • we do not have a user but only his id
  • that we have an API fetchUser which, given an id, queries an endpoint that returns the corresponding User
import * as T from 'fp-ts/Task'

interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}

const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})

declare const fetchUser: (id: number) => T.Task<User>

const userId = 1
const followerId = 3

const result = addFollower(fetchUser(followerId))(fetchUser(userId)) // does not compile

I can't use addFollower anymore! How can we proceed?

If only we had a function with the following signature:

declare const addFollowerAsync: (
follower: T.Task<User>
) => (user: T.Task<User>) => T.Task<User>

we could proceed with ease:

import * as T from 'fp-ts/Task'

interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}

declare const fetchUser: (id: number) => T.Task<User>

declare const addFollowerAsync: (
follower: T.Task<User>
) => (user: T.Task<User>) => T.Task<User>

const userId = 1
const followerId = 3

// const result: T.Task<User>
const result = addFollowerAsync(fetchUser(followerId))(fetchUser(userId)) // now compiles

We can obviously implement addFollowerAsync manually, but is it possible instead to find a transformation which starting with a function like addFollower: (follower: User) => (user: User): User returns a function like addFollowerAsync: (follower: Task<User>) => (user: Task<User>) => Task<User>?

More generally what we would like to have is a transformation, call it liftA2, which beginning with a function g: (b: B) => (c: C) => D returns a function with the following signature:

liftA2(g): (fb: F<B>) => (fc: F<C>) => F<D>
liftA2

How can we obtain it? Given that g is now a unary function, we can leverage the functor instance and the good old map:

map(g): (fb: F<B>) => F<(c: C) => D>
liftA2 (first step)

Now we are blocked: there's no legal operation the functor instance provides us to "unpack" the type F<(c: C) => D> into (fc: F<C>) => F<D>.

We need to introduce a new operation ap which realizes this unpacking:

declare const ap: <A>(fa: Task<A>) => <B>(fab: Task<(a: A) => B>) => Task<B>

Note. Why is it names "ap"? Because it can be seen like some sort of function application.

// `apply` applies a function to a value
declare const apply: <A>(a: A) => <B>(f: (a: A) => B) => B

declare const ap: <A>(a: Task<A>) => <B>(f: Task<(a: A) => B>) => Task<B>
// `ap` applies a function wrapped into an effect to a value wrapped into an effect

Now that we have ap we can define liftA2:

import { pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Task'

const liftA2 = <B, C, D>(g: (b: B) => (c: C) => D) => (fb: T.Task<B>) => (
fc: T.Task<C>
): T.Task<D> => pipe(fb, T.map(g), T.ap(fc))

interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}

const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})

// const addFollowerAsync: (fb: T.Task<User>) => (fc: T.Task<User>) => T.Task<User>
const addFollowerAsync = liftA2(addFollower)

and finally, we can compose fetchUser with the previous result:

import { flow, pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Task'

const liftA2 = <B, C, D>(g: (b: B) => (c: C) => D) => (fb: T.Task<B>) => (
fc: T.Task<C>
): T.Task<D> => pipe(fb, T.map(g), T.ap(fc))

interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}

const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})

declare const fetchUser: (id: number) => T.Task<User>

// const program: (id: number) => (fc: T.Task<User>) => T.Task<User>
const program = flow(fetchUser, liftA2(addFollower))

const userId = 1
const followerId = 3

// const result: T.Task<User>
const result = program(followerId)(fetchUser(userId))

We have found a standard procedure to compose two functions f: (a: A) => F<B>, g: (b: B, c: C) => D:

  1. we transform g through currying in a function g: (b: B) => (c: C) => D
  2. we define the ap function for the effect F (library function)
  3. we define the utility function liftA2 for the effect F (library function)
  4. we obtain the composition flow(f, liftA2(g))

Let's see how's the ap operation implemented for some of the type constructors we've already seen:

Example (F = ReadonlyArray)

import { increment, pipe } from 'fp-ts/function'

const ap = <A>(fa: ReadonlyArray<A>) => <B>(
fab: ReadonlyArray<(a: A) => B>
): ReadonlyArray<B> => {
const out: Array<B> = []
for (const f of fab) {
for (const a of fa) {
out.push(f(a))
}
}
return out
}

const double = (n: number): number => n * 2

pipe([double, increment], ap([1, 2, 3]), console.log) // => [ 2, 4, 6, 2, 3, 4 ]

Example (F = Option)

import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'

const ap = <A>(fa: O.Option<A>) => <B>(
fab: O.Option<(a: A) => B>
): O.Option<B> =>
pipe(
fab,
O.match(
() => O.none,
(f) =>
pipe(
fa,
O.match(
() => O.none,
(a) => O.some(f(a))
)
)
)
)

const double = (n: number): number => n * 2

pipe(O.some(double), ap(O.some(1)), console.log) // => some(2)
pipe(O.some(double), ap(O.none), console.log) // => none
pipe(O.none, ap(O.some(1)), console.log) // => none
pipe(O.none, ap(O.none), console.log) // => none

Example (F = IO)

import { IO } from 'fp-ts/IO'

const ap = <A>(fa: IO<A>) => <B>(fab: IO<(a: A) => B>): IO<B> => () => {
const f = fab()
const a = fa()
return f(a)
}

Example (F = Task)

import { Task } from 'fp-ts/Task'

const ap = <A>(fa: Task<A>) => <B>(fab: Task<(a: A) => B>): Task<B> => () =>
Promise.all([fab(), fa()]).then(([f, a]) => f(a))

Example (F = Reader)

import { Reader } from 'fp-ts/Reader'

const ap = <R, A>(fa: Reader<R, A>) => <B>(
fab: Reader<R, (a: A) => B>
): Reader<R, B> => (r) => {
const f = fab(r)
const a = fa(r)
return f(a)
}

We've seen how with ap we can manage functions with two parameters, but what happens with functions that take three parameters? Do we need yet another abstraction?

Good news is no, map and ap are sufficient:

import { pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Task'

const liftA3 = <B, C, D, E>(f: (b: B) => (c: C) => (d: D) => E) => (
fb: T.Task<B>
) => (fc: T.Task<C>) => (fd: T.Task<D>): T.Task<E> =>
pipe(fb, T.map(f), T.ap(fc), T.ap(fd))

const liftA4 = <B, C, D, E, F>(
f: (b: B) => (c: C) => (d: D) => (e: E) => F
) => (fb: T.Task<B>) => (fc: T.Task<C>) => (fd: T.Task<D>) => (
fe: T.Task<E>
): T.Task<F> => pipe(fb, T.map(f), T.ap(fc), T.ap(fd), T.ap(fe))

// etc...

Now we cam update ore "composition table":

Program fProgram gComposition
purepureg ∘ f
effectfulpure (unary)map(g) ∘ f
effectfulpure, n-aryliftAn(g) ∘ f

The of operation

Now we know that given two function f: (a: A) => F<B>, g: (b: B, c: C) => D we can obtain the composition h:

h: (a: A) => (fc: F<C>) => F<D>

To execute h we need a new value of type A and a value of type F<C>.

But what happens if, instead of having a value of type F<C>, for the second parameter fc we only have a value of type C?

It would be helpful to have an operation which can transform a value of type C in a value of type F<C> in order to use h.

Let's introduce such operation, called of (other synonyms: pure, return):

declare const of: <C>(c: C) => F<C>

In literature the term applicative functors is used for the type constructors which admith both the ap and of operations.

Let's see how of is defined for some type constructors we've already seen:

Example (F = ReadonlyArray)

const of = <A>(a: A): ReadonlyArray<A> => [a]

Example (F = Option)

import * as O from 'fp-ts/Option'

const of = <A>(a: A): O.Option<A> => O.some(a)

Example (F = IO)

import { IO } from 'fp-ts/IO'

const of = <A>(a: A): IO<A> => () => a

Example (F = Task)

import { Task } from 'fp-ts/Task'

const of = <A>(a: A): Task<A> => () => Promise.resolve(a)

Example (F = Reader)

import { Reader } from 'fp-ts/Reader'

const of = <R, A>(a: A): Reader<R, A> => () => a

Demo

05_applicative.ts

Applicative functors compose

Applicative functors compose, meaning that given two applicative functors F and G, their composition F<G<A>> is still an applicative functor.

Example (F = Task, G = Option)

The of of the composition is the composition of the ofs:

import { flow } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import * as T from 'fp-ts/Task'

type TaskOption<A> = T.Task<O.Option<A>>

const of: <A>(a: A) => TaskOption<A> = flow(O.of, T.of)

the ap of the composition is obtained by the following pattern:

const ap = <A>(
fa: TaskOption<A>
): (<B>(fab: TaskOption<(a: A) => B>) => TaskOption<B>) =>
flow(
T.map((gab) => (ga: O.Option<A>) => O.ap(ga)(gab)),
T.ap(fa)
)

Do applicative functors solve the general problem?

Not yet. There's one last very important case to consider: when both programs are effectful.

Yet again we need something more, in the following chapter we'll talk about one of the most important abstractions in functional programming: monads.